﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using HandyControl.Data;
using HandyControl.Expression.Drawing;
using HandyControl.Tools;
using HandyControl.Tools.Extension;

namespace HandyControl.Controls;

public class DashedBorder : Decorator
{
    private bool _useComplexRenderCodePath;

    private Pen GeometryPenCache { get; set; }

    private Pen LeftPenCache { get; set; }

    private Pen RightPenCache { get; set; }

    private Pen TopPenCache { get; set; }

    private Pen BottomPenCache { get; set; }

    private StreamGeometry BackgroundGeometryCache { get; set; }

    private StreamGeometry BorderGeometryCache { get; set; }

    private static void OnClearPenCache(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var border = (DashedBorder) d;
        border.LeftPenCache = null;
        border.RightPenCache = null;
        border.TopPenCache = null;
        border.BottomPenCache = null;
        border.GeometryPenCache = null;
    }

    public static readonly DependencyProperty BorderThicknessProperty = DependencyProperty.Register(
        nameof(BorderThickness), typeof(Thickness), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Thickness),
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            OnClearPenCache));

    public Thickness BorderThickness
    {
        get => (Thickness) GetValue(BorderThicknessProperty);
        set => SetValue(BorderThicknessProperty, value);
    }

    public static readonly DependencyProperty BorderDashThicknessProperty = DependencyProperty.Register(
        nameof(BorderDashThickness), typeof(double), typeof(DashedBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box,
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            OnClearPenCache));

    public double BorderDashThickness
    {
        get => (double) GetValue(BorderDashThicknessProperty);
        set => SetValue(BorderDashThicknessProperty, value);
    }

    public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register(
        nameof(Padding), typeof(Thickness), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Thickness),
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

    public Thickness Padding
    {
        get => (Thickness) GetValue(PaddingProperty);
        set => SetValue(PaddingProperty, value);
    }

    public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(
        nameof(CornerRadius), typeof(CornerRadius), typeof(DashedBorder), new FrameworkPropertyMetadata(default(CornerRadius),
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

    public CornerRadius CornerRadius
    {
        get => (CornerRadius) GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }

    public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register(
        nameof(BorderBrush), typeof(Brush), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Brush),
            FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
            OnClearPenCache));

    public Brush BorderBrush
    {
        get => (Brush) GetValue(BorderBrushProperty);
        set => SetValue(BorderBrushProperty, value);
    }

    public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(
        nameof(Background), typeof(Brush), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Brush),
            FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
            OnClearPenCache));

    public Brush Background
    {
        get => (Brush) GetValue(BackgroundProperty);
        set => SetValue(BackgroundProperty, value);
    }

    public static readonly DependencyProperty BorderDashArrayProperty = DependencyProperty.Register(
        nameof(BorderDashArray), typeof(DoubleCollection), typeof(DashedBorder), new FrameworkPropertyMetadata(default(DoubleCollection),
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            OnClearPenCache));

    public DoubleCollection BorderDashArray
    {
        get => (DoubleCollection) GetValue(BorderDashArrayProperty);
        set => SetValue(BorderDashArrayProperty, value);
    }

    public static readonly DependencyProperty BorderDashCapProperty = DependencyProperty.Register(
        nameof(BorderDashCap), typeof(PenLineCap), typeof(DashedBorder), new FrameworkPropertyMetadata(default(PenLineCap),
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            OnClearPenCache));

    public PenLineCap BorderDashCap
    {
        get => (PenLineCap) GetValue(BorderDashCapProperty);
        set => SetValue(BorderDashCapProperty, value);
    }

    public static readonly DependencyProperty BorderDashOffsetProperty = DependencyProperty.Register(
        nameof(BorderDashOffset), typeof(double), typeof(DashedBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box,
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            OnClearPenCache));

    public double BorderDashOffset
    {
        get => (double) GetValue(BorderDashOffsetProperty);
        set => SetValue(BorderDashOffsetProperty, value);
    }

    private static Size ConvertThickness2Size(Thickness th) => new(th.Left + th.Right, th.Top + th.Bottom);

    private static Rect DeflateRect(Rect rt, Thickness thick) => new(rt.Left + thick.Left,
        rt.Top + thick.Top,
        Math.Max(0.0, rt.Width - thick.Left - thick.Right),
        Math.Max(0.0, rt.Height - thick.Top - thick.Bottom));

    private static bool AreUniformCorners(CornerRadius borderRadii)
    {
        var topLeft = borderRadii.TopLeft;
        return MathHelper.AreClose(topLeft, borderRadii.TopRight) &&
               MathHelper.AreClose(topLeft, borderRadii.BottomLeft) &&
               MathHelper.AreClose(topLeft, borderRadii.BottomRight);
    }

    private static void GenerateGeometry(StreamGeometryContext ctx, Rect rect, in Radii radii)
    {
        var topLeft = new Point(radii.LeftTop, 0);
        var topRight = new Point(rect.Width - radii.RightTop, 0);
        var rightTop = new Point(rect.Width, radii.TopRight);
        var rightBottom = new Point(rect.Width, rect.Height - radii.BottomRight);
        var bottomRight = new Point(rect.Width - radii.RightBottom, rect.Height);
        var bottomLeft = new Point(radii.LeftBottom, rect.Height);
        var leftBottom = new Point(0, rect.Height - radii.BottomLeft);
        var leftTop = new Point(0, radii.TopLeft);

        //  top edge
        if (topLeft.X > topRight.X)
        {
            var v = radii.LeftTop / (radii.LeftTop + radii.RightTop) * rect.Width;
            topLeft.X = v;
            topRight.X = v;
        }

        //  right edge
        if (rightTop.Y > rightBottom.Y)
        {
            var v = radii.TopRight / (radii.TopRight + radii.BottomRight) * rect.Height;
            rightTop.Y = v;
            rightBottom.Y = v;
        }

        //  bottom edge
        if (bottomRight.X < bottomLeft.X)
        {
            var v = radii.LeftBottom / (radii.LeftBottom + radii.RightBottom) * rect.Width;
            bottomRight.X = v;
            bottomLeft.X = v;
        }

        //  left edge
        if (leftBottom.Y < leftTop.Y)
        {
            var v = radii.TopLeft / (radii.TopLeft + radii.BottomLeft) * rect.Height;
            leftBottom.Y = v;
            leftTop.Y = v;
        }

        //  add on offsets
        var offset = new Vector(rect.TopLeft.X, rect.TopLeft.Y);
        topLeft += offset;
        topRight += offset;
        rightTop += offset;
        rightBottom += offset;
        bottomRight += offset;
        bottomLeft += offset;
        leftBottom += offset;
        leftTop += offset;

        //  create the border geometry
        ctx.BeginFigure(topLeft, true, true);

        //  Top line
        ctx.LineTo(topRight, true, false);

        //  Upper-right corner
        var radiusX = rect.TopRight.X - topRight.X;
        var radiusY = rightTop.Y - rect.TopRight.Y;
        if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
        {
            ctx.ArcTo(rightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
        }

        //  Right line
        ctx.LineTo(rightBottom, true, false);

        //  Lower-right corner
        radiusX = rect.BottomRight.X - bottomRight.X;
        radiusY = rect.BottomRight.Y - rightBottom.Y;
        if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
        {
            ctx.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
        }

        //  Bottom line
        ctx.LineTo(bottomLeft, true, false);

        //  Lower-left corner
        radiusX = bottomLeft.X - rect.BottomLeft.X;
        radiusY = rect.BottomLeft.Y - leftBottom.Y;
        if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
        {
            ctx.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
        }

        //  Left line
        ctx.LineTo(leftTop, true, false);

        //  Upper-left corner
        radiusX = topLeft.X - rect.TopLeft.X;
        radiusY = leftTop.Y - rect.TopLeft.Y;
        if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
        {
            ctx.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var child = Child;
        var borderThickness = BorderThickness;
        var padding = Padding;

        if (UseLayoutRounding)
        {
            var dpiScaleX = DpiHelper.DeviceDpiX;
            var dpiScaleY = DpiHelper.DeviceDpiY;

            borderThickness = new Thickness(
                DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX),
                DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY),
                DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
        }

        var borderSize = ConvertThickness2Size(borderThickness);
        var paddingSize = ConvertThickness2Size(padding);
        var mySize = new Size();

        if (child != null)
        {
            var combined = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height);
            var childConstraint = new Size(Math.Max(0.0, constraint.Width - combined.Width), Math.Max(0.0, constraint.Height - combined.Height));

            child.Measure(childConstraint);
            var childSize = child.DesiredSize;

            mySize.Width = childSize.Width + combined.Width;
            mySize.Height = childSize.Height + combined.Height;
        }
        else
        {
            mySize = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height);
        }

        return mySize;
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var borderThickness = BorderThickness;

        if (UseLayoutRounding)
        {
            var dpiScaleX = DpiHelper.DeviceDpiX;
            var dpiScaleY = DpiHelper.DeviceDpiY;

            borderThickness = new Thickness(
                DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX),
                DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY),
                DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
        }

        var boundRect = new Rect(arrangeSize);
        var innerRect = DeflateRect(boundRect, borderThickness);

        var child = Child;
        if (child != null)
        {
            var padding = Padding;
            var childRect = DeflateRect(innerRect, padding);
            child.Arrange(childRect);
        }

        var radii = CornerRadius;
        var borderBrush = BorderBrush;
        var uniformCorners = AreUniformCorners(radii);

        _useComplexRenderCodePath = !uniformCorners;
        if (!_useComplexRenderCodePath && borderBrush != null)
        {
            _useComplexRenderCodePath = !MathHelper.IsZero(radii.TopLeft) && !borderThickness.IsUniform();
        }

        if (_useComplexRenderCodePath)
        {
            var innerRadii = new Radii(radii, borderThickness, false);
            StreamGeometry backgroundGeometry = null;

            if (!MathHelper.IsZero(innerRect.Width) && !MathHelper.IsZero(innerRect.Height))
            {
                backgroundGeometry = new StreamGeometry();

                using (var ctx = backgroundGeometry.Open())
                {
                    GenerateGeometry(ctx, innerRect, innerRadii);
                }

                backgroundGeometry.Freeze();
                BackgroundGeometryCache = backgroundGeometry;
            }
            else
            {
                BackgroundGeometryCache = null;
            }

            if (!MathHelper.IsZero(boundRect.Width) && !MathHelper.IsZero(boundRect.Height))
            {
                var outerRadii = new Radii(radii, borderThickness, true);
                var borderGeometry = new StreamGeometry();

                using (var ctx = borderGeometry.Open())
                {
                    GenerateGeometry(ctx, boundRect, outerRadii);

                    if (backgroundGeometry != null)
                    {
                        GenerateGeometry(ctx, innerRect, innerRadii);
                    }
                }

                borderGeometry.Freeze();
                BorderGeometryCache = borderGeometry;
            }
            else
            {
                BorderGeometryCache = null;
            }
        }
        else
        {
            BackgroundGeometryCache = null;
            BorderGeometryCache = null;
        }

        return arrangeSize;
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        var background = Background;
        var borderBrush = BorderBrush;
        var useLayoutRounding = UseLayoutRounding;

        if (_useComplexRenderCodePath)
        {
            var borderGeometry = BorderGeometryCache;
            if (borderGeometry != null && borderBrush != null)
            {
                var pen = GeometryPenCache;
                if (pen == null)
                {
                    pen = new Pen
                    {
                        Brush = borderBrush,
                        Thickness = BorderDashThickness,
                        DashCap = BorderDashCap,
                        DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                    };

                    if (borderBrush.IsFrozen)
                    {
                        pen.Freeze();
                    }

                    GeometryPenCache = pen;
                }

                drawingContext.DrawGeometry(null, pen, borderGeometry);
            }

            var backgroundGeometry = BackgroundGeometryCache;
            if (backgroundGeometry != null && background != null)
            {
                drawingContext.DrawGeometry(background, null, backgroundGeometry);
            }
        }
        else
        {
            var dpiScaleX = DpiHelper.DeviceDpiX;
            var dpiScaleY = DpiHelper.DeviceDpiY;

            var borderThickness = BorderThickness;
            var cornerRadius = CornerRadius;
            var outerCornerRadius = cornerRadius.TopLeft;
            var roundedCorners = !MathHelper.IsZero(outerCornerRadius);

            if (!borderThickness.IsZero() && borderBrush != null)
            {
                var pen = LeftPenCache;
                if (pen == null)
                {
                    pen = new Pen
                    {
                        Brush = borderBrush,
                        Thickness = useLayoutRounding
                            ? DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX)
                            : borderThickness.Left,
                        DashCap = BorderDashCap,
                        DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                    };

                    if (borderBrush.IsFrozen)
                    {
                        pen.Freeze();
                    }

                    LeftPenCache = pen;
                }

                double halfThickness;
                var renderSize = RenderSize;

                if (borderThickness.IsUniform())
                {
                    halfThickness = pen.Thickness * 0.5;
                    var rect = new Rect(new Point(halfThickness, halfThickness), new Point(renderSize.Width - halfThickness, renderSize.Height - halfThickness));

                    if (roundedCorners)
                    {
                        drawingContext.DrawRoundedRectangle(null, pen, rect, outerCornerRadius, outerCornerRadius);
                    }
                    else
                    {
                        drawingContext.DrawRectangle(null, pen, rect);
                    }
                }
                else
                {
                    if (MathHelper.GreaterThan(borderThickness.Left, 0))
                    {
                        halfThickness = pen.Thickness * 0.5;
                        drawingContext.DrawLine(pen, new Point(halfThickness, 0), new Point(halfThickness, renderSize.Height));
                    }

                    if (MathHelper.GreaterThan(borderThickness.Right, 0))
                    {
                        pen = RightPenCache;
                        if (pen == null)
                        {
                            pen = new Pen
                            {
                                Brush = borderBrush,
                                Thickness = useLayoutRounding
                                    ? DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX)
                                    : borderThickness.Right,
                                DashCap = BorderDashCap,
                                DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                            };

                            if (borderBrush.IsFrozen)
                            {
                                pen.Freeze();
                            }

                            RightPenCache = pen;
                        }

                        halfThickness = pen.Thickness * 0.5;
                        drawingContext.DrawLine(pen, new Point(renderSize.Width - halfThickness, 0), new Point(renderSize.Width - halfThickness, renderSize.Height));
                    }

                    if (MathHelper.GreaterThan(borderThickness.Top, 0))
                    {
                        pen = TopPenCache;
                        if (pen == null)
                        {
                            pen = new Pen
                            {
                                Brush = borderBrush,
                                Thickness = useLayoutRounding
                                    ? DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY)
                                    : borderThickness.Top,
                                DashCap = BorderDashCap,
                                DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                            };

                            if (borderBrush.IsFrozen)
                            {
                                pen.Freeze();
                            }

                            TopPenCache = pen;
                        }

                        halfThickness = pen.Thickness * 0.5;
                        drawingContext.DrawLine(pen, new Point(0, halfThickness), new Point(renderSize.Width, halfThickness));
                    }

                    if (MathHelper.GreaterThan(borderThickness.Bottom, 0))
                    {
                        pen = BottomPenCache;
                        if (pen == null)
                        {
                            pen = new Pen
                            {
                                Brush = borderBrush,
                                Thickness = useLayoutRounding
                                    ? DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY)
                                    : borderThickness.Bottom,
                                DashCap = BorderDashCap,
                                DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                            };

                            if (borderBrush.IsFrozen)
                            {
                                pen.Freeze();
                            }

                            BottomPenCache = pen;
                        }

                        halfThickness = pen.Thickness * 0.5;
                        drawingContext.DrawLine(pen, new Point(0, renderSize.Height - halfThickness), new Point(renderSize.Width, renderSize.Height - halfThickness));
                    }
                }
            }

            if (background != null)
            {
                Point ptTL, ptBR;

                if (useLayoutRounding)
                {
                    ptTL = new Point(DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX), DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY));
                    ptBR = new Point(RenderSize.Width - DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                        RenderSize.Height - DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
                }
                else
                {
                    ptTL = new Point(borderThickness.Left, borderThickness.Top);
                    ptBR = new Point(RenderSize.Width - borderThickness.Right, RenderSize.Height - borderThickness.Bottom);
                }

                if (ptBR.X > ptTL.X && ptBR.Y > ptTL.Y)
                {
                    if (roundedCorners)
                    {
                        var innerRadii = new Radii(cornerRadius, borderThickness, false);
                        var innerCornerRadius = innerRadii.TopLeft;
                        drawingContext.DrawRoundedRectangle(background, null, new Rect(ptTL, ptBR), innerCornerRadius, innerCornerRadius);
                    }
                    else
                    {
                        drawingContext.DrawRectangle(background, null, new Rect(ptTL, ptBR));
                    }
                }
            }
        }
    }

    private readonly struct Radii
    {
        internal Radii(CornerRadius radii, Thickness borders, bool outer)
        {
            var left = 0.5 * borders.Left;
            var top = 0.5 * borders.Top;
            var right = 0.5 * borders.Right;
            var bottom = 0.5 * borders.Bottom;

            if (outer)
            {
                if (MathHelper.IsZero(radii.TopLeft))
                {
                    LeftTop = TopLeft = 0.0;
                }
                else
                {
                    LeftTop = radii.TopLeft + left;
                    TopLeft = radii.TopLeft + top;
                }
                if (MathHelper.IsZero(radii.TopRight))
                {
                    TopRight = RightTop = 0.0;
                }
                else
                {
                    TopRight = radii.TopRight + top;
                    RightTop = radii.TopRight + right;
                }
                if (MathHelper.IsZero(radii.BottomRight))
                {
                    RightBottom = BottomRight = 0.0;
                }
                else
                {
                    RightBottom = radii.BottomRight + right;
                    BottomRight = radii.BottomRight + bottom;
                }
                if (MathHelper.IsZero(radii.BottomLeft))
                {
                    BottomLeft = LeftBottom = 0.0;
                }
                else
                {
                    BottomLeft = radii.BottomLeft + bottom;
                    LeftBottom = radii.BottomLeft + left;
                }
            }
            else
            {
                LeftTop = Math.Max(0.0, radii.TopLeft - left);
                TopLeft = Math.Max(0.0, radii.TopLeft - top);
                TopRight = Math.Max(0.0, radii.TopRight - top);
                RightTop = Math.Max(0.0, radii.TopRight - right);
                RightBottom = Math.Max(0.0, radii.BottomRight - right);
                BottomRight = Math.Max(0.0, radii.BottomRight - bottom);
                BottomLeft = Math.Max(0.0, radii.BottomLeft - bottom);
                LeftBottom = Math.Max(0.0, radii.BottomLeft - left);
            }
        }

        internal readonly double LeftTop;
        internal readonly double TopLeft;
        internal readonly double TopRight;
        internal readonly double RightTop;
        internal readonly double RightBottom;
        internal readonly double BottomRight;
        internal readonly double BottomLeft;
        internal readonly double LeftBottom;
    }
}
